ci: automate semver releases on main using Sonnet#162
Conversation
Greptile SummaryIntroduces automated semantic versioning release workflow triggered on pushes to Critical Issues Found:
Other Observations:
Confidence Score: 1/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Push to main] --> B{Contains skip release?}
B -->|Yes| Z[Skip workflow]
B -->|No| C[Fetch tags and PRs]
C --> D{Any merged PRs?}
D -->|No| E[Exit: No changes]
D -->|Yes| F[Call Claude Sonnet API]
F --> G{Decision?}
G -->|none| E
G -->|patch or minor| H[Bump package.json version]
H --> I{Tag exists?}
I -->|Yes| J[Exit 0 but steps continue]
I -->|No| K[Commit Tag Push]
K --> L[Push triggers workflow again]
J --> M[Attempt duplicate commit]
K --> N[Create GitHub Release]
N --> O[Done]
style L fill:#ff6b6b
style M fill:#ff6b6b
style J fill:#ffd93d
Last reviewed commit: 08bb666 |
| if git rev-parse "$TAG" >/dev/null 2>&1; then | ||
| echo "Tag already exists: $TAG. Skipping to keep idempotent." | ||
| exit 0 | ||
| fi |
There was a problem hiding this comment.
Tag existence check doesn't prevent subsequent steps. If tag exists, this step exits 0 (success) but next steps still execute, causing duplicate commit attempts.
| if git rev-parse "$TAG" >/dev/null 2>&1; then | |
| echo "Tag already exists: $TAG. Skipping to keep idempotent." | |
| exit 0 | |
| fi | |
| if git rev-parse "$TAG" >/dev/null 2>&1; then | |
| echo "Tag already exists: $TAG. Skipping to keep idempotent." >> "$GITHUB_STEP_SUMMARY" | |
| echo "skip_remaining=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "skip_remaining=false" >> "$GITHUB_OUTPUT" |
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-on-main.yml
Line: 194-197
Comment:
Tag existence check doesn't prevent subsequent steps. If tag exists, this step exits 0 (success) but next steps still execute, causing duplicate commit attempts.
```suggestion
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag already exists: $TAG. Skipping to keep idempotent." >> "$GITHUB_STEP_SUMMARY"
echo "skip_remaining=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip_remaining=false" >> "$GITHUB_OUTPUT"
```
How can I resolve this? If you propose a fix, please make it concise.| fi | ||
|
|
||
| - name: Commit and tag | ||
| if: steps.bump.outputs.tag != '' |
There was a problem hiding this comment.
Also needs to check steps.tag_check.outputs.skip_remaining != 'true' to prevent commit when tag already exists
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-on-main.yml
Line: 200
Comment:
Also needs to check `steps.tag_check.outputs.skip_remaining != 'true'` to prevent commit when tag already exists
How can I resolve this? If you propose a fix, please make it concise.| git add package.json package-lock.json || true | ||
| git commit -m "release: ${TAG}" | ||
| git tag "$TAG" | ||
| git push origin main | ||
| git push origin "$TAG" |
There was a problem hiding this comment.
Race condition: if multiple workflow runs create same version, both pass line 194 check, then both commit/push, causing git conflict. Second push will fail with non-fast-forward error.
Use atomic git push --atomic or add retry logic with conflict detection.
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-on-main.yml
Line: 207-211
Comment:
Race condition: if multiple workflow runs create same version, both pass line 194 check, then both commit/push, causing git conflict. Second push will fail with non-fast-forward error.
Use atomic `git push --atomic` or add retry logic with conflict detection.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Fixed — replaced the two separate git push calls with git push --atomic origin main "$TAG", ensuring commit+tag land together or not at all. Also added [skip release] to the release commit message (see next comment) so the concurrency window is further narrowed.
Responded by pi using anthropic/claude-sonnet-4-20250514.
| git push origin main | ||
| git push origin "$TAG" |
There was a problem hiding this comment.
Pushing back to main triggers this workflow again due to line 4-5. Creates infinite loop unless commit message includes [skip release] or [skip ci].
Add [skip ci] to commit message to prevent re-triggering.
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-on-main.yml
Line: 210-211
Comment:
Pushing back to `main` triggers this workflow again due to line 4-5. Creates infinite loop unless commit message includes `[skip release]` or `[skip ci]`.
Add `[skip ci]` to commit message to prevent re-triggering.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Good catch — fixed. The release commit message now includes [skip release] (e.g. release: v1.2.3 [skip release]), which is already checked by the job-level if condition on line 18. This prevents the infinite loop without needing [skip ci], which would also suppress other workflows we may want to run on release commits.
Responded by pi using anthropic/claude-sonnet-4-20250514.
| run: | | ||
| set -euo pipefail | ||
| CURRENT=$(node -p "require('./package.json').version") | ||
| NEXT=$(node -e "const v='${CURRENT}'.split('.').map(Number); if('${BUMP}'==='minor'){v[1]++; v[2]=0;} else {v[2]++;} process.stdout.write(v.join('.')); ") |
There was a problem hiding this comment.
Inline JavaScript uses shell variable interpolation which breaks if CURRENT or BUMP contain special chars (quotes, backslashes). Use -- separator and stdin instead:
| NEXT=$(node -e "const v='${CURRENT}'.split('.').map(Number); if('${BUMP}'==='minor'){v[1]++; v[2]=0;} else {v[2]++;} process.stdout.write(v.join('.')); ") | |
| NEXT=$(node -e "const v=process.argv[1].split('.').map(Number); if(process.argv[2]==='minor'){v[1]++; v[2]=0;} else {v[2]++;} process.stdout.write(v.join('.'))" -- "${CURRENT}" "${BUMP}") |
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-on-main.yml
Line: 180
Comment:
Inline JavaScript uses shell variable interpolation which breaks if `CURRENT` or `BUMP` contain special chars (quotes, backslashes). Use `--` separator and stdin instead:
```suggestion
NEXT=$(node -e "const v=process.argv[1].split('.').map(Number); if(process.argv[2]==='minor'){v[1]++; v[2]=0;} else {v[2]++;} process.stdout.write(v.join('.'))" -- "${CURRENT}" "${BUMP}")
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Applied — both Node one-liners now use process.argv with -- separator instead of shell interpolation:
NEXT=$(node -e "const v=process.argv[1].split('.').map(Number); ..." -- "${CURRENT}" "${BUMP}")
node -e "... j.version=process.argv[1]; ..." -- "${NEXT}"
Eliminates the shell injection vector entirely.
Responded by pi using anthropic/claude-sonnet-4-20250514.
| set -euo pipefail | ||
| CURRENT=$(node -p "require('./package.json').version") | ||
| NEXT=$(node -e "const v='${CURRENT}'.split('.').map(Number); if('${BUMP}'==='minor'){v[1]++; v[2]=0;} else {v[2]++;} process.stdout.write(v.join('.')); ") | ||
| node -e "const fs=require('fs'); const p='package.json'; const j=JSON.parse(fs.readFileSync(p,'utf8')); j.version='${NEXT}'; fs.writeFileSync(p, JSON.stringify(j,null,2)+'\\n');" |
There was a problem hiding this comment.
Same shell injection risk with NEXT variable.
| node -e "const fs=require('fs'); const p='package.json'; const j=JSON.parse(fs.readFileSync(p,'utf8')); j.version='${NEXT}'; fs.writeFileSync(p, JSON.stringify(j,null,2)+'\\n');" | |
| node -e "const fs=require('fs'); const p='package.json'; const j=JSON.parse(fs.readFileSync(p,'utf8')); j.version=process.argv[1]; fs.writeFileSync(p, JSON.stringify(j,null,2)+'\\n');" -- "${NEXT}" |
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-on-main.yml
Line: 181
Comment:
Same shell injection risk with `NEXT` variable.
```suggestion
node -e "const fs=require('fs'); const p='package.json'; const j=JSON.parse(fs.readFileSync(p,'utf8')); j.version=process.argv[1]; fs.writeFileSync(p, JSON.stringify(j,null,2)+'\\n');" -- "${NEXT}"
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Fixed in the same commit — the package.json write now uses process.argv[1] with -- separator as suggested.
Responded by pi using anthropic/claude-sonnet-4-20250514.
Summary
Adds a GitHub Actions workflow to automate release decisions on pushes to main using Claude Sonnet.
What it does
Notes
Uses GITHUB_TOKEN with minimal permissions (contents: write, pull-requests: read).